{
  lib,
  pkgs,
  ...
}:
{
  options.services.github-runners = lib.mkOption {
    description = ''
      Multiple GitHub Runners.
    '';
    example = {
      runner1 = {
        enable = true;
        url = "https://github.com/owner/repo";
        name = "runner1";
        tokenFile = "/secrets/token1";
      };

      runner2 = {
        enable = true;
        url = "https://github.com/owner/repo";
        name = "runner2";
        tokenFile = "/secrets/token2";
      };
    };
    default = { };
    type = lib.types.attrsOf (
      lib.types.submodule (
        { name, config, ... }:
        {
          options = {
            enable = lib.mkOption {
              default = false;
              example = true;
              description = ''
                Whether to enable GitHub Actions runner.

                Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
                [About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners).
              '';
              type = lib.types.bool;
            };

            url = lib.mkOption {
              type = lib.types.str;
              description = ''
                Repository to add the runner to.

                Changing this option triggers a new runner registration.

                IMPORTANT: If your token is org-wide (not per repository), you need to
                provide a github org link, not a single repository, so do it like this
                `https://github.com/nixos`, not like this
                `https://github.com/nixos/nixpkgs`.
                Otherwise, you are going to get a `404 NotFound`
                from `POST https://api.github.com/actions/runner-registration`
                in the configure script.
              '';
              example = "https://github.com/nixos/nixpkgs";
            };

            tokenFile = lib.mkOption {
              type = lib.types.path;
              description = ''
                The full path to a file which contains either

                * a fine-grained personal access token (PAT),
                * a classic PAT
                * or a runner registration token

                Changing this option or the `tokenFile`’s content triggers a new runner registration.

                We suggest using the fine-grained PATs. A runner registration token is valid
                only for 1 hour after creation, so the next time the runner configuration changes
                this will give you hard-to-debug HTTP 404 errors in the configure step.

                The file should contain exactly one line with the token without any newline.
                (Use `echo -n '…token…' > …token file…` to make sure no newlines sneak in.)

                If the file contains a PAT, the service creates a new registration token
                on startup as needed.
                If a registration token is given, it can be used to re-register a runner of the same
                name but is time-limited as noted above.

                For fine-grained PATs:

                Give it "Read and Write access to organization/repository self hosted runners",
                depending on whether it is organization wide or per-repository. You might have to
                experiment a little, fine-grained PATs are a `beta` Github feature and still subject
                to change; nonetheless they are the best option at the moment.

                For classic PATs:

                Make sure the PAT has a scope of `admin:org` for organization-wide registrations
                or a scope of `repo` for a single repository.

                For runner registration tokens:

                Nothing special needs to be done, but updating will break after one hour,
                so these are not recommended.
              '';
              example = "/run/secrets/github-runner/nixos.token";
            };

            name = lib.mkOption {
              type = lib.types.nullOr lib.types.str;
              description = ''
                Name of the runner to configure. If null, defaults to the hostname.

                Changing this option triggers a new runner registration.
              '';
              example = "nixos";
              default = name;
            };

            runnerGroup = lib.mkOption {
              type = lib.types.nullOr lib.types.str;
              description = ''
                Name of the runner group to add this runner to (defaults to the default runner group).

                Changing this option triggers a new runner registration.
              '';
              default = null;
            };

            extraLabels = lib.mkOption {
              type = lib.types.listOf lib.types.str;
              description = ''
                Extra labels in addition to the default (unless disabled through the `noDefaultLabels` option).

                Changing this option triggers a new runner registration.
              '';
              example = lib.literalExpression ''[ "nixos" ]'';
              default = [ ];
            };

            noDefaultLabels = lib.mkOption {
              type = lib.types.bool;
              description = ''
                Disables adding the default labels. Also see the `extraLabels` option.

                Changing this option triggers a new runner registration.
              '';
              default = false;
            };

            replace = lib.mkOption {
              type = lib.types.bool;
              description = ''
                Replace any existing runner with the same name.

                Without this flag, registering a new runner with the same name fails.
              '';
              default = false;
            };

            extraPackages = lib.mkOption {
              type = lib.types.listOf lib.types.package;
              description = ''
                Extra packages to add to `PATH` of the service to make them available to workflows.
              '';
              default = [ ];
            };

            extraEnvironment = lib.mkOption {
              type = lib.types.attrs;
              description = ''
                Extra environment variables to set for the runner, as an attrset.
              '';
              example = {
                GIT_CONFIG = "/path/to/git/config";
              };
              default = { };
            };

            serviceOverrides = lib.mkOption {
              type = lib.types.attrs;
              description = ''
                Modify the systemd service. Can be used to, e.g., adjust the sandboxing options.
                See {manpage}`systemd.exec(5)` for more options.
              '';
              example = {
                ProtectHome = false;
                RestrictAddressFamilies = [ "AF_PACKET" ];
              };
              default = { };
            };

            package = lib.mkPackageOption pkgs "github-runner" { } // {
              apply = pkg: pkg.override { inherit (config) nodeRuntimes; };
            };

            ephemeral = lib.mkOption {
              type = lib.types.bool;
              description = ''
                If enabled, causes the following behavior:

                - Passes the `--ephemeral` flag to the runner configuration script
                - De-registers and stops the runner with GitHub after it has processed one job
                - On stop, systemd wipes the runtime directory (this always happens, even without using the ephemeral option)
                - Restarts the service after its successful exit
                - On start, wipes the state directory and configures a new runner

                You should only enable this option if `tokenFile` points to a file which contains a
                personal access token (PAT). If you're using the option with a registration token, restarting the
                service will fail as soon as the registration token expired.

                Changing this option triggers a new runner registration.
              '';
              default = false;
            };

            user = lib.mkOption {
              type = lib.types.nullOr lib.types.str;
              description = ''
                User under which to run the service.

                If this option and the `group` option is set to `null`,
                the service runs as a dynamically allocated user.

                Also see the `group` option for an overview on the effects of the `user` and `group` settings.
              '';
              default = null;
              defaultText = lib.literalExpression "username";
            };

            group = lib.mkOption {
              type = lib.types.nullOr lib.types.str;
              description = ''
                Group under which to run the service.

                The effect of this option depends on the value of the `user` option:

                - `group == null` and `user == null`:
                  The service runs with a dynamically allocated user and group.
                - `group == null` and `user != null`:
                  The service runs as the given user and its default group.
                - `group != null` and `user == null`:
                  This configuration is invalid. In this case, the service would use the given group
                  but run as root implicitly. If this is really what you want, set `user = "root"` explicitly.
              '';
              default = null;
              defaultText = lib.literalExpression "groupname";
            };

            workDir = lib.mkOption {
              type = with lib.types; nullOr str;
              description = ''
                Working directory, available as `$GITHUB_WORKSPACE` during workflow runs
                and used as a default for [repository checkouts](https://github.com/actions/checkout).
                The service cleans this directory on every service start.

                A value of `null` will default to the systemd `RuntimeDirectory`.

                Changing this option triggers a new runner registration.
              '';
              default = null;
            };

            nodeRuntimes = lib.mkOption {
              type =
                with lib.types;
                nonEmptyListOf (enum [
                  "node20"
                  "node24"
                ]);
              default = [
                "node20"
                "node24"
              ];
              description = ''
                List of Node.js runtimes the runner should support.
              '';
            };
          };
        }
      )
    );
  };
}
